package com.wqj.kyee.gps.nmea;

import android.support.annotation.NonNull;
import android.util.Log;

import com.wqj.kyee.gps.ui.SatelliteCN0Activity;
import com.wqj.kyee.gps.util.Constants;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Created by rabby on 2017/12/17.
* */
public class NMEA {

       // java interfaces
    interface SentenceParser {
        boolean parse(String [] tokens, GPSPosition position);
    }

    interface GroupSentencesParser {
        boolean parse(String sentence, ArrayList<SVInfo> svList);
    }

       // utils
//       static float Latitude2Decimal(String lat, String NS) {
//               float med = Float.parseFloat(lat.substring(2))/60.0f;
//               med +=  Float.parseFloat(lat.substring(0, 2));
//               if(NS.startsWith("S")) {
//                           med = -med;
//                   }
//               return med;
//       }
//
//       static float Longitude2Decimal(String lon, String WE) {
//               float med = Float.parseFloat(lon.substring(3))/60.0f;
//               med +=  Float.parseFloat(lon.substring(0, 3));
//               if(WE.startsWith("W")) {
//                           med = -med;
//                   }
//               return med;
//       }

       static double Latitude2Degree(String lat, String NS) {
            double med = 0;
            try {
                med = Double.parseDouble(lat);
                med = Math.floor(med / 100) + (med % 100) / 60;
                if(NS.startsWith("S")) {
                    med = -med;
                }
            }catch (Exception e) {
                med = 0;
            }
            return med;
       }

       static double Longitude2Degree(String lon, String WE) {
           double med = 0;
           try {
               med = Double.parseDouble(lon);
               med = Math.floor(med / 100) + (med % 100) / 60;
               if(WE.startsWith("W")) {
                   med = -med;
               }
           }catch (Exception e) {
               med = 0;
           }

           return med;
       }


       // parsers
       class GGA implements SentenceParser {
       public boolean parse(String [] tokens, GPSPosition position) {
           try {
               position.time = Float.parseFloat(tokens[1]);
               position.lat = Latitude2Degree(tokens[2], tokens[3]);
               position.lon = Longitude2Degree(tokens[4], tokens[5]);
               position.quality = Integer.parseInt(tokens[6]);
               position.altitude = Float.parseFloat(tokens[9]);
           } catch (Exception e)
           {
               return false;
           }
           return true;
       }
}

       class GPGGL implements SentenceParser {
       public boolean parse(String [] tokens, GPSPosition position) {
                           position.lat = Latitude2Degree(tokens[1], tokens[2]);
                           position.lon = Longitude2Degree(tokens[3], tokens[4]);
                           position.time = Float.parseFloat(tokens[5]);
                           return true;
       }

}

        class RMC implements SentenceParser {
            public boolean parse(String [] tokens, GPSPosition position) {
               try {
                           position.time = Float.parseFloat(tokens[1]);
                           position.lat = Latitude2Degree(tokens[3], tokens[4]);
                           position.lon = Longitude2Degree(tokens[5], tokens[6]);
                           position.velocity = Float.parseFloat(tokens[7]);
                           position.velocity = position.velocity * 0.514444f; //Kn to m/s
                           position.dir = Float.parseFloat(tokens[8]);
                   } catch (Exception ex) {
                           //String test = "";
                            return false;
                   }
               return true;
            }
        }

       class AHRS implements SentenceParser {

           @Override
           public boolean parse(String[] tokens, GPSPosition position) {
               try {
                   position.dir = Float.parseFloat(tokens[4]);
                   return true;
               } catch (Exception ex) {
                   return false;
               }
           }
       }

       class GPVTG implements SentenceParser {
       public boolean parse(String [] tokens, GPSPosition position) {
                           position.dir = Float.parseFloat(tokens[3]);
                           return true;
                   }
}

    class GPRMZ implements SentenceParser {
    public boolean parse(String [] tokens, GPSPosition position) {
                       position.altitude = Float.parseFloat(tokens[1]);
                       return true;
        }
    }

    class GSV implements GroupSentencesParser {

        @Override
        public boolean parse(String sentence, ArrayList<SVInfo> svList) {

            String[] tokens;
            String stripCKS;
            try {
                //item = item.replaceAll("\r | \n", "");
                stripCKS =  sentence.substring(0, sentence.lastIndexOf(Constants.NMEA_CHS_SEPARATOR));
                tokens = stripCKS.split(Constants.NMEA_FIELD_SEPARATOR);
                int totalNum = Integer.parseInt(tokens[3]);
                if (totalNum > 0) {
                    for (int index = 4; index < tokens.length; index += 4) {
                        SVInfo sv = new SVInfo();
                        sv.PRN = Integer.parseInt(tokens[index]);
                        try {
                            if (!tokens[index + 3].isEmpty())
                                sv.CN0 = Integer.parseInt(tokens[index + 3]);
                            else
                                sv.CN0 = 0;
                        } catch (Exception e) {
                            sv.CN0 = 0;
                        }
                        svList.add(sv);
                    }
                }

            }catch (Exception e)
            {
                Log.e(TAG, "parse gsv error. line = " + sentence);
            }

            if (svList.isEmpty())
                return false;
            else
                return true;
        }
    }

    public class GPSPosition {
        public float time = 0.0f;
        public double lat = 0.0f;
        public double lon = 0.0f;
        public boolean fixed = false;
        public int quality = 0;
        public float dir = 0.0f;
        public float altitude = 0.0f;
        public float velocity = 0.0f;

        public void updatefix() {
           //fixed = quality > 0;
            fixed = true;
        }

        public String toString() {
           return String.format("POSITION: lat: %f, lon: %f, time: %f, Q: %d, dir: %f, alt: %f, vel: %f(m/s)", lat, lon, time, quality, dir, altitude, velocity);
        }
    }

    public class SVInfo implements Comparable<SVInfo>{
        public int PRN;
        public int CN0;

        public boolean isGPS() {
            return ((this.PRN >= Constants.SatelliteNumbering.MIN_GPS) && (this.PRN <= Constants.SatelliteNumbering.MAX_GPS)) ? true : false;
        }

        public boolean isSBAS() {
            return ((this.PRN >= Constants.SatelliteNumbering.MIN_SBAS) && (this.PRN <= Constants.SatelliteNumbering.MAX_SBAS)) ? true : false;
        }

        public boolean isGLONASS() {
            return ((this.PRN >= Constants.SatelliteNumbering.MIN_GLONASS) && (this.PRN <= Constants.SatelliteNumbering.MAX_GLONASS)) ? true : false;
        }

        public boolean isBEIDOU() {
            return ((this.PRN >= Constants.SatelliteNumbering.MIN_BEIDOU) && (this.PRN <= Constants.SatelliteNumbering.MAX_BEIDOU)) ? true : false;
        }

        public boolean isQZSS() {
            return ((this.PRN >= Constants.SatelliteNumbering.MIN_QZSS) && (this.PRN <= Constants.SatelliteNumbering.MAX_QZSS)) ? true : false;
        }

        public boolean isGALILEO() {
            return ((this.PRN >= Constants.SatelliteNumbering.MIN_GALILEO) && (this.PRN <= Constants.SatelliteNumbering.MAX_GALILEO)) ? true : false;
        }

        @Override
        public int compareTo(@NonNull SVInfo svInfo) {
            return Integer.compare(svInfo.PRN, this.PRN);
        }

        public int getColorValue() {
            if (this.isGPS()) {
                return SatelliteCN0Activity.GROUP_COLORS[0];
            } else if(this.isSBAS()) {
                return SatelliteCN0Activity.GROUP_COLORS[1];
            } else if (this.isBEIDOU()) {
                return SatelliteCN0Activity.GROUP_COLORS[2];
            } else if (this.isGLONASS()) {
                return SatelliteCN0Activity.GROUP_COLORS[3];
            } else if (this.isGALILEO()) {
                return SatelliteCN0Activity.GROUP_COLORS[4];
            } else {
                return SatelliteCN0Activity.GROUP_COLORS[5];
            }
        }
    }

    //GPSPosition position = new GPSPosition();

    private static final Map<String, SentenceParser> sentenceParsers = new HashMap<String, SentenceParser>();
    private static final Map<String, GroupSentencesParser> groupParsers = new HashMap<String, GroupSentencesParser>();

    public NMEA() {
        sentenceParsers.put(Constants.NMEA_TOKEN_GGA, new GGA());
        //sentenceParsers.put("GPGGL", new GPGGL());
        sentenceParsers.put(Constants.NMEA_TOKEN_RMC, new RMC());
        sentenceParsers.put(Constants.NMEA_PROPRIETARY_TOKEN_AHRS, new AHRS());
        //sentenceParsers.put("GPRMZ", new GPRMZ());
        //only really good GPS devices have this sentence but ...
        //        sentenceParsers.put("GPVTG", new GPVTG());

        groupParsers.put(Constants.NMEA_TOKEN_GSV, new GSV());
    }

    public static final String TAG = "NMEA";


    public GPSPosition parse(String raw) {
        GPSPosition currentPos = new GPSPosition();
        String line = raw.replaceAll("\r|\n", "");

        //if(line.startsWith("$")) {
        if (verifySentence(line)) {

            String[] tokens;
           try {
               String nmea = line.substring(1);
               tokens = nmea.split(Constants.NMEA_FIELD_SEPARATOR);
               String type = tokens[0].substring(2);
               //TODO check crc
               if (sentenceParsers.containsKey(type)) {
                   if (sentenceParsers.get(type).parse(tokens, currentPos) == false) {
                       Log.e(TAG, "Parse NMEA line Err, line = " + raw);
                       return null;
                   } else
                       currentPos.updatefix();
               }

           } catch (Exception e){
               Log.e(TAG, "Parse NMEA line Err, Raw = " + raw);
           }
        } else {
            Log.e(TAG, "NMEA line Err, line = " + raw);
        }
       return currentPos;
    }

    public  ArrayList<SVInfo> parse(String type, ArrayList<String> group) {
        ArrayList<SVInfo> svList = new ArrayList<>();

        for (String item : group) {
            if (groupParsers.containsKey(type)) {

                String stripCRNL = item.replaceAll("\r|\n", "");

                if (verifySentence(stripCRNL)) {
                    groupParsers.get(type).parse(item, svList);
                } else {
                    Log.e(TAG, "NMEA line err, line = " + item);
                }
            }
        }

        return svList;
    }

    private static int calculateChecksum(String sentence) throws UnsupportedEncodingException {
        byte[] bytes = sentence.substring(1, sentence.length() - 3).getBytes("US-ASCII");
        int checksum = 0;
        for (byte b : bytes) {
            checksum ^= b;
        }
        return checksum;
    }

    private static boolean verifySentence(String sentence) {
        if (sentence == null)
            return false;
        try {
            ExMatcher matcher = new ExMatcher(GENERAL_SENTENCE.matcher(sentence));
            if (matcher.matches()) {
                String type = matcher.nextString("type");
                String content = matcher.nextString("content");
                int expected_checksum = matcher.nextHexInt("checksum");
                int actual_checksum = calculateChecksum(sentence);

                if (actual_checksum != expected_checksum) {
                    Log.e(TAG, "NMEA checksum error, sentence = " + sentence);
                    return false;
                }
            }
        } catch (Exception e) {
            return  false;
        }

        return true;
    }

    private static final String HEX_INT = "[0-9a-fA-F]";
    private static final Pattern GENERAL_SENTENCE = Pattern.compile("^\\$(\\w{5}),(.*)[*](" + HEX_INT + "{2})$");
    private static class ExMatcher {
        Matcher original;
        int index;

        ExMatcher(Matcher original) {
            this.original = original;
            reset();
        }

        void reset() { index = 1; }

        boolean matches() { return original.matches(); }

        String nextString(String name) { return original.group(index++); }

        Float nextFloat(String name, Float defaultValue) {
            Float next = nextFloat(name);
            return next == null ? defaultValue : next;
        }

        Float nextFloat(String name) {
            String next = nextString(name);
            return next == null ? null : Float.parseFloat(next);
        }

        Integer nextInt(String name) {
            String next = nextString(name);
            return next == null ? null : Integer.parseInt(next);
        }

        Integer nextHexInt(String name) {
            String next = nextString(name);
            return next == null ? null : Integer.parseInt(next, 16);
        }
    }
}
